未确定数目的参数

  对于有些函数而言,我们没办法确定在各个调用中所期望的所有参数的个数和类型。声明这种函数的方式就是在参数表的最后用省略号(...)结束,省略号表示“还可能有另外一些参数”。例如,

int printf(const char* ...);

这描述的是C标准库函数printf()(21.8节),对它的一个调用至少必须有一个参数,一个char*,但是还可以有,也可以没有其他参数。例如,

    printf("Hello, world!\n");
    printf("My name is %s %s\n", first_name, second_name);
    printf("%d + %d = %d\n", 2, 3, 5);

这样的函数必须依赖于一些编译时无法使用的信息去解释它的参数表。对于printf()而言,其第一个参数是个格式串,其中包含了一些特殊字符序列,这些字符使printf()能够正确地处理其他参数。%s表示“期待一个char*参数”,%d表示“期待一个int参数”。然而,一般来说,编译器是没有办法知道这些情况的,它不能保证所期待的参数真的就在那里,也无法保证某个参数具有合适的类型。例如,

    #include <stdio.h>
    int main()
    {
        printf("My name is %s %s\n", 2);
    }

将能通过编译,但(在最好情况下)将导致某些看起来很奇怪的输出(请试一试)。

  事情很清楚,如果一个参数没有声明,编译器就没有信息去对它执行标准的类型检查和转换。在这种情况下,一个char或short将作为int传递,float将作为double传递。这些做法未必是程序员所期望的。

  一个设计良好的程序至多只需要极少的几个这种参数类型没有完全刻画的函数。在大部分情况下,我们都可以利用重载函数和使用默认参数的函数来处理类型检查的问题。如果没有这些机制,在一些情况下就无法刻画参数的类型。只有在那些参数数目和参数类型都有变化的情况下才需要省略号。省略号最常见的用途是描述C库函数的界面,这些都是在C++提供了替代方式之前做出来的:

    int fprintf(FILE*, const char* ...);            // 取自<cstdio>
    int execl(const char* ...);                     // 取自UNIX头文件

<cstdarg>里提供了一组标准的宏,专门用于在这种函数里访问未加描述的参数。现在考虑写一个出错函数,它有一个int参数指明错误的严重性,随后是任意个字符串。这里的想法是将各个词分别作为字符串参数传递,进而组合起这个错误信息。字符串参数列表的最后需要有一个指向char的空指针。

    extern void error(int ...);
    extern char* itoa(int, char[]);        // 见6.6[17]

    const char* Null_cp = 0;

    int main(int argc, char* argv[])
    {
        switch(argc) {
            case 1:
                error(0, argv[0], Null_cp);
                break;
            case 2:
                error(0, argv[0], argv[1], Null_cp);
                break;
            default:
                char buffer[8];
                error(1, argv[0], "with", itoa(argc-1, buffer), "arguments", Null_cp);
        }
        // ... 
    }

函数itoa()返回它的整数参数的字符串表示。

  请注意,用整数0作为结束符可能产生不可移植问题:在某些实现中,整数0和空指针的表示形式可能不同。这一情况也说明,一旦使用省略号抑制了类型检查,程序员就必须直接面对一些难以琢磨的额外工作。

  出错函数可以像下面这样定义:

    void error(int severity ...)        // "severity"(严重性)后跟空指针结束的char*列表
    {
        va_list ap;
        va_start(ap, severity);         // arg开始

        for(;;) {
            char* p = va_arg(ap, char*);
            if(p == 0) break;
            cerr << p << ' ';
        }

        va_end(ap);                     // arg清理

        cerr << '\n';
        if(severity) exit(severity);
    }

首先,通过调用va_start()定义并初始化一个va_list。宏va_start以一个va_list的名字和函数的最后一个有名形式参数的名字作为参数。宏va_arg()用于按顺序提取出各个无名参数。在每次调用va_arg()时,程序员都必须提供一个类型,va_arg()假定这就是被传递的实际参数的类型,但一般说它并没有办法去保证这一点。从一个使用过va_start()的函数中退出之前,必须调用一次va_end()。这是因为va_start()可能以某种方式修改了堆栈,这种修改可能导致返回无法完成,va_end()能将有关的修改复原。

🔚